import geopandas as gpd
import pandas as pd
import os
## find the directory of the python (assures compatibility)
python_directory = os.path.abspath("")
from tqdm import tqdm
import matplotlib.pyplot as plt
from matplotlib.lines import Line2D
import folium
import copy
import math
import statistics
import numpy as np
import warnings
warnings.filterwarnings("ignore")
from models import Optimise_PANsCatchment_Schools, reset_parameters, create_custom_legend_handles, extract_PANs, Random_PANsCatchment_schools
This includes adding the information required for models in the analysis phase. This includes the estimated number of 5 year olds:
## load the PANs
PANs = pd.read_csv(rf"{python_directory}/data/Yr7_admissions.csv")
PANs_2024 = extract_PANs(PANs, 2024)
target_PAN = extract_PANs(PANs, 2024)
## load the maps
schools = gpd.read_file(rf"{python_directory}/data/brighton_sec_schools.geojson")
lsoa = gpd.read_file(rf"{python_directory}/data/BrightonLSOA_Clean.geojson")
students = gpd.read_file(rf"{python_directory}/data/BrightonLSOA_Clean.geojson")
catchment_a = gpd.read_file(rf"{python_directory}/data/catchment_01.geojson")
catchment_b = gpd.read_file(rf"{python_directory}/data/catchment_02.geojson")
catchment_c = gpd.read_file(rf"{python_directory}/data/catchment_03.geojson")
catchment_list = [catchment_a, catchment_b, catchment_c]
## transform lsoa and students dataframes to EPSG:27700 (For the UK)
lsoa.to_crs(epsg="27700", inplace=True)
students.to_crs(epsg="27700", inplace=True)
for catchment in catchment_list:
catchment["geometry"] = catchment.buffer(0)
catchment.to_crs(epsg="27700", inplace=True)
## Print the current CRS
print(f"CRS | schools = {schools.crs}, LSOA = {lsoa.crs}, students LSOA = {students.crs}")
CRS | schools = EPSG:27700, LSOA = EPSG:27700, students LSOA = EPSG:27700
## define colours
colours = {
"Blatchington Mill School": "steelblue",
"Brighton Aldridge Community Academy": "orange",
"Cardinal Newman Catholic School": "limegreen",
"Dorothy Stringer School": "firebrick",
"Hove Park School and Sixth Form Centre": "mediumpurple",
"King's School": "sienna",
"Longhill High School": "palevioletred",
"Patcham High School": "gray",
"Portslade Aldridge Community Academy": "darkkhaki",
"Varndean School": "darkturquoise",
}
## assign school colours
schools["colour"] = [colours[schools.at[i, "establishment_name"]] for i in schools.index]
## Calculate the estimated 5 year students
reset_parameters(catchment_a, schools, students)
## Generate interactive map for the number of 5 year olds
m = students.explore(column="5_est", tooltip="5_est", name="Estimated 5 year olds", legend=True, min_zoom=12)
schools.explore(
m=m, column="establishment_name", tooltip="establishment_name", color=schools["colour"], name="Schools",
marker_kwds={"radius": 5}, style_kwds={"fillOpacity":1}, legend=True
)
display(m)
m = None
The analysis uses a model that assign students to schools using Euclidean distances as the driver for the assignment process. The model iterates through schools and selects the closes LSOA within the school's catchment zone. It assigns the stydebts in that LSOA to the school. This process is repeated across all schools until taking any further LSOA exceeds the respective school's allocated PAN.
In more detail, the model applies the following workflow:
The model generates a spatial distribution of school catchments that may not align with the catchment zones due to PAN restrictions. By comparing the results to the catchment zones, we can identify:
This allows for making conclusions on the suitability of the catchment areas and PAN restrictions used in 2024.
We consider the catchment zone alternative b (labelled "Catchment alternative 2" in the layers of the plotted map). Each LSOA is assigned to the catchment zone which contains the majority of the LSOA; it finds the best fit for assigning LSOAs to catchments. This leads to cases where the LSOA catchment zones do not strictly align with the drawn catchment zones (as the drawn catchment zones do not strictly follow the outlines of LSOAs).
## reset the parameters based on the second catchment alternative
reset_parameters(catchment_b, schools, students)
target_PAN["King's School"] = 0
target_PAN["Cardinal Newman Catholic School"] = 0
print("Attributes created and reset in all DataFrames")
print("--------------------")
print("LAOAs visualised based on catchment area ID")
m = students.explore(column="catchment_ID", categorical=True, cmap="tab10", tooltip="catchment_ID", legend=True, name="LSOA alternative 2", min_zoom=12)
catchment_b.explore(m=m, tooltip="catchment_ID", style_kwds={"fill": False, "color": "black"}, name="Catchment alternative 2")
schools.explore(
m=m, column="establishment_name", tooltip="establishment_name", color=schools["colour"], name="Schools",
marker_kwds={"radius": 5}, style_kwds={"fillOpacity":1}, legend=False
)
folium.LayerControl().add_to(m)
m
Attributes created and reset in all DataFrames -------------------- LAOAs visualised based on catchment area ID
## Run the model
reset_parameters(schools=schools, students=students, catchment=catchment_b)
Optimise_PANsCatchment_Schools(schools=schools, students_lsoa=students, target_PAN=target_PAN)
print("Finalised model run")
Finalised model run
The students in LSOAs are generally assigned to a school within their catchment areas. Yet, we observe a few notable exceptions:
Overall, all the schools end up attracting more students than their PANs to make up for "Cardinal Newman" and "King's". These schools end up with high mean distances travelled (weighed to the number of students in each LSOA) for students outside of their catchment.
## Plot the map
students["colour"] = [colours[students.at[i, "school"]] for i in students.index]
students["edgecolour"] = ["white" for i in students.index]
schools["colour"] = [colours[schools.at[i, "establishment_name"]] for i in schools.index]
fig = students.plot(color=students["colour"], edgecolor=students["edgecolour"], linewidth=0.1, legend=True, figsize=(20,10))
schools.plot(ax=fig, color=schools["colour"], edgecolor='black', markersize=60, legend=True)
catchment_b.plot(ax=fig, facecolor="none", edgecolor="black", label="Catchment")
plt.title("Map 1.1: 2024 PANs run spatial results")
plt.legend(handles=create_custom_legend_handles(colours))
plt.show()
plt.close()
for i in students.index:
if students.at[i, "external"] == True:
students.at[i, "colour"] = None
students.at[i, "colour"] = "black"
fig = students.plot(color=students["colour"], edgecolor=students["edgecolour"], linewidth=0.1, legend=True, figsize=(20,10))
schools.plot(ax=fig, color=schools["colour"], edgecolor='black', markersize=60, legend=True)
catchment_b.plot(ax=fig, facecolor="none", edgecolor="black", label="Catchment")
plt.title("Map 1.2: 2024 PANs run spatial results (LSOAs assigned after PAN saturation mapped in black)")
plt.legend(handles=create_custom_legend_handles(colours))
plt.show()
plt.close()
## plot target PAN numbers
## create ordered lists
students_total_plot, PANs_plot, students_outside_catchment, distances, distances_outside_catchment = list(), list(), list(), list(), list()
x_axis_list = list()
for school_str in PANs["school"]:
students_total_plot.append(int(schools[schools["establishment_name"] == school_str]["students_total"]))
PANs_plot.append(target_PAN[school_str])
## find all LSOAs outside the catchment
LSOAs_outside_catchment = students[(students["school"] == school_str) & (students["catchment_ID"] != students["catchment_ID_school"])]
students_outside_catchment.append(sum( LSOAs_outside_catchment["5_est"] ) )
if target_PAN[school_str] == 0: distances.append(0)
else: distances.append( ( sum(students[students["school"] == school_str]["distx5_est"]) / sum(students[students["school"] == school_str]["5_est"]) ) * 0.000621371)
if len(LSOAs_outside_catchment.index) > 0:
distances_outside_catchment.append( (sum(LSOAs_outside_catchment["distx5_est"]) / sum(LSOAs_outside_catchment["5_est"])) * 0.000621371)
else:
distances_outside_catchment.append(0)
x_axis_list.append(school_str)
## Generate the graphs
x_axis = np.arange(len(x_axis_list))
width = 0.25
fig = plt.figure(figsize=(14, 7))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
ax1.set_title("Graph 1.1: Model results compared to 2024 PANs")
ax1.bar(x_axis - width, students_total_plot, width, color="steelblue", label="Model students")
ax1.bar(x_axis , PANs_plot, width, color="orange", label="Target PANs")
ax1.bar(x_axis + width, students_outside_catchment, width, color="red", label="Model students outside catchment")
ax1.set_xticks(x_axis, x_axis_list, rotation=90)
ax1.set_ylabel("Students")
ax1.legend()
ax2.set_title("Graph 1.2: Mean distances travelled")
ax2.bar(x_axis - width / 2 , distances, width, color="steelblue", label="All students")
ax2.bar(x_axis + width / 2 , distances_outside_catchment, width, color="red", label="Students outside catchment")
ax2.set_xticks(x_axis, x_axis_list, rotation=90)
ax2.set_ylabel("Distance (miles)")
ax2.legend()
plt.show()
plt.close()
In this application, we modify the PANs as follows:
target_PAN = {
"Blatchington Mill School": 330,
"Brighton Aldridge Community Academy": 165,
"Cardinal Newman Catholic School": 360,
"Dorothy Stringer School": 330,
"Hove Park School and Sixth Form Centre": 180,
"King's School": 165,
"Longhill High School": 165,
"Patcham High School": 225,
"Portslade Aldridge Community Academy": 220,
"Varndean School": 300,
}
df_dict = {
"School": target_PAN.keys(),
"2024": PANs_2024.values(),
"Modified": target_PAN.values()
}
display(pd.DataFrame(df_dict))
| School | 2024 | Modified | |
|---|---|---|---|
| 0 | Blatchington Mill School | 330 | 330 |
| 1 | Brighton Aldridge Community Academy | 180 | 165 |
| 2 | Cardinal Newman Catholic School | 360 | 360 |
| 3 | Dorothy Stringer School | 330 | 330 |
| 4 | Hove Park School and Sixth Form Centre | 180 | 180 |
| 5 | King's School | 165 | 165 |
| 6 | Longhill High School | 270 | 165 |
| 7 | Patcham High School | 225 | 225 |
| 8 | Portslade Aldridge Community Academy | 220 | 220 |
| 9 | Varndean School | 300 | 300 |
## reset the parameters based on the second catchment alternative
reset_parameters(catchment_b, schools, students)
target_PAN["King's School"] = 0
target_PAN["Cardinal Newman Catholic School"] = 0
print("Attributes created and reset in all DataFrames")
print("--------------------")
## Run the model
reset_parameters(schools=schools, students=students, catchment=catchment_b)
Optimise_PANsCatchment_Schools(schools=schools, students_lsoa=students, target_PAN=target_PAN)
print("Finalised model run")
Attributes created and reset in all DataFrames -------------------- Finalised model run
The students in LSOAs are generally assigned to a school within their catchment areas. In comparison to the 2024 PANs run:
Similar to the 2024 PANs run, all the schools end up attracting more students than their PANs to make up for "Cardinal Newman" and "King's". These schools end up with high mean distances travelled (weighed to the number of students in each LSOA) for students outside of their catchment.
## Plot the map
students["colour"] = [colours[students.at[i, "school"]] for i in students.index]
students["edgecolour"] = ["white" for i in students.index]
schools["colour"] = [colours[schools.at[i, "establishment_name"]] for i in schools.index]
fig = students.plot(color=students["colour"], edgecolor=students["edgecolour"], linewidth=0.1, legend=True, figsize=(20,10))
schools.plot(ax=fig, color=schools["colour"], edgecolor='black', markersize=60, legend=True)
catchment_b.plot(ax=fig, facecolor="none", edgecolor="black", label="Catchment")
plt.title("Map 2.1: Modified PANs run spatial results")
plt.legend(handles=create_custom_legend_handles(colours))
plt.show()
plt.close()
for i in students.index:
if students.at[i, "external"] == True:
students.at[i, "colour"] = None
students.at[i, "colour"] = "black"
fig = students.plot(color=students["colour"], edgecolor=students["edgecolour"], linewidth=0.1, legend=True, figsize=(20,10))
schools.plot(ax=fig, color=schools["colour"], edgecolor='black', markersize=60, legend=True)
catchment_b.plot(ax=fig, facecolor="none", edgecolor="black", label="Catchment")
plt.title("Map 2.2: Modified PANs run spatial results (LSOAs assigned after PAN saturation mapped in black)")
plt.legend(handles=create_custom_legend_handles(colours))
plt.show()
plt.close()
## plot target PAN numbers
## create ordered lists
students_total_plot, PANs_plot, students_outside_catchment, distances, distances_outside_catchment = list(), list(), list(), list(), list()
x_axis_list = list()
for school_str in PANs["school"]:
students_total_plot.append(int(schools[schools["establishment_name"] == school_str]["students_total"]))
PANs_plot.append(target_PAN[school_str])
## find all LSOAs outside the catchment
LSOAs_outside_catchment = students[(students["school"] == school_str) & (students["catchment_ID"] != students["catchment_ID_school"])]
students_outside_catchment.append(sum( LSOAs_outside_catchment["5_est"] ) )
if target_PAN[school_str] == 0: distances.append(0)
else: distances.append( ( sum(students[students["school"] == school_str]["distx5_est"]) / sum(students[students["school"] == school_str]["5_est"]) ) * 0.000621371)
if len(LSOAs_outside_catchment.index) > 0:
distances_outside_catchment.append( (sum(LSOAs_outside_catchment["distx5_est"]) / sum(LSOAs_outside_catchment["5_est"])) * 0.000621371)
else:
distances_outside_catchment.append(0)
x_axis_list.append(school_str)
## Generate the graphs
x_axis = np.arange(len(x_axis_list))
width = 0.25
fig = plt.figure(figsize=(14,7))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
ax1.set_title("Graph 2.1: Modified PANs run results compared to 2024 PANs")
ax1.bar(x_axis - width, students_total_plot, width, color="steelblue", label="Model students")
ax1.bar(x_axis , PANs_plot, width, color="orange", label="Target PANs")
ax1.bar(x_axis + width, students_outside_catchment, width, color="red", label="Model students outside catchment")
ax1.set_xticks(x_axis, x_axis_list, rotation=90)
ax1.set_ylabel("Students")
ax1.legend()
ax2.set_title("Graph 2.2: Modified PANs run results compared to distances travelled")
ax2.bar(x_axis - width / 2 , distances, width, color="steelblue", label="All students")
ax2.bar(x_axis + width / 2 , distances_outside_catchment, width, color="red", label="Students outside catchment")
ax2.set_xticks(x_axis, x_axis_list, rotation=90)
ax2.set_ylabel("Distance (miles)")
ax2.legend()
plt.show()
plt.close()
To showcase the mean distance travelled to each school, we assign the schools randomly to each school given its target PANs. We then calculate the mean distance travelled to each school from its LSOAs (weighed to the number of students in each LSOA).
We run the model for 20 time to account for the stochasticity in the selection of LSOAs. Foe each run, we show a sample map for the random distribution of LSOAs and the results for the mean weighed distances travelled (miles) in each run per school.
target_PAN = copy.deepcopy(PANs_2024)
## reset the parameters based on the second catchment alternative
reset_parameters(catchment_b, schools, students)
print("Attributes created and reset in all DataFrames")
print("--------------------")
## Run the model
print("Running the model")
reset_parameters(schools=schools, students=students, catchment=catchment_b)
dist_dict, dist_external_dict, students_3_miles_dict = Random_PANsCatchment_schools(schools=schools, students_lsoa=students, target_PAN=target_PAN, n_runs=20)
print("--------------------")
Attributes created and reset in all DataFrames -------------------- Running the model
100%|██████████| 20/20 [00:47<00:00, 2.40s/it]
--------------------
## Plot the map
students["colour"] = [colours[students.at[i, "school"]] for i in students.index]
students["edgecolour"] = ["white" for i in students.index]
schools["colour"] = [colours[schools.at[i, "establishment_name"]] for i in schools.index]
fig = students.plot(color=students["colour"], edgecolor=students["edgecolour"], linewidth=0.1, legend=True, figsize=(20,10))
schools.plot(ax=fig, color=schools["colour"], edgecolor='black', markersize=60, legend=True)
catchment_b.plot(ax=fig, facecolor="none", edgecolor="black", label="Catchment")
plt.title("Map 3: Spatial outcomes of a sample random assignment of LSOAs run")
plt.legend(handles=create_custom_legend_handles(colours))
plt.show()
plt.close()
## Show the results per run
print("Result weighed mean distances travelled by students per run (miles)")
dist_df = pd.DataFrame(dist_dict)
dist_external_df = pd.DataFrame(dist_external_dict)
students_3_miles_df = pd.DataFrame(students_3_miles_dict)
display(dist_df)
print("Number of students travelling more than 3 miles per run")
display(students_3_miles_df)
Result weighed mean distances travelled by students per run (miles)
| Blatchington Mill School | Brighton Aldridge Community Academy | Cardinal Newman Catholic School | Dorothy Stringer School | Hove Park School and Sixth Form Centre | King's School | Longhill High School | Patcham High School | Portslade Aldridge Community Academy | Varndean School | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.945071 | 1.619689 | 1.264535 | 2.086664 | 0.802953 | 1.404452 | 2.140684 | 1.378243 | 0.393287 | 0.984105 |
| 1 | 0.951674 | 1.569262 | 1.372755 | 2.212073 | 0.734563 | 1.217681 | 1.980210 | 1.278152 | 0.485377 | 1.003108 |
| 2 | 1.087663 | 1.592890 | 1.209733 | 2.046033 | 0.688456 | 1.330507 | 1.964596 | 1.114967 | 0.453977 | 0.988712 |
| 3 | 1.205338 | 1.363903 | 1.156162 | 1.933373 | 0.633433 | 1.219916 | 1.883122 | 1.588747 | 0.484384 | 0.916836 |
| 4 | 1.126021 | 1.454235 | 1.140443 | 1.927938 | 0.710054 | 1.188888 | 1.755258 | 1.511434 | 0.402350 | 0.862883 |
| 5 | 1.028349 | 1.402028 | 1.316441 | 1.778746 | 0.703216 | 1.626897 | 2.032421 | 1.130299 | 0.455929 | 0.871842 |
| 6 | 1.493539 | 1.595288 | 1.360936 | 2.229045 | 0.502239 | 1.167431 | 2.329135 | 1.123246 | 0.436513 | 1.113122 |
| 7 | 1.073945 | 1.795225 | 1.209068 | 1.770081 | 0.673046 | 0.810244 | 1.804625 | 1.290120 | 0.464565 | 0.856326 |
| 8 | 1.257761 | 1.460972 | 1.035143 | 2.329704 | 0.737671 | 0.880895 | 2.235482 | 1.460106 | 0.433704 | 0.781637 |
| 9 | 0.871738 | 1.317792 | 1.198974 | 1.901771 | 0.824457 | 0.908532 | 2.091724 | 1.206384 | 0.422695 | 1.057704 |
| 10 | 0.995141 | 1.556588 | 1.160126 | 1.813491 | 0.466754 | 1.404005 | 1.808636 | 1.154482 | 0.400818 | 1.051585 |
| 11 | 1.091795 | 1.495399 | 0.985945 | 2.412554 | 0.678437 | 0.973008 | 2.354766 | 0.963116 | 0.337080 | 1.043309 |
| 12 | 1.293053 | 1.446097 | 1.369543 | 1.900896 | 0.583612 | 1.094053 | 2.027983 | 1.165133 | 0.382641 | 0.855677 |
| 13 | 1.061120 | 2.186550 | 0.967488 | 1.993934 | 0.794418 | 1.051013 | 2.134455 | 1.424760 | 0.464565 | 0.948888 |
| 14 | 1.297102 | 1.712507 | 1.232200 | 1.904189 | 0.569488 | 1.323006 | 2.223429 | 1.191772 | 0.376562 | 0.997691 |
| 15 | 0.932869 | 1.608216 | 1.405716 | 2.066123 | 0.573842 | 1.015184 | 1.947419 | 0.900714 | 0.461858 | 0.973938 |
| 16 | 1.248552 | 1.670798 | 1.273624 | 2.103089 | 0.677047 | 1.416541 | 2.725741 | 1.122685 | 0.355086 | 0.804384 |
| 17 | 1.142600 | 1.731271 | 1.279750 | 1.879741 | 0.676908 | 1.563999 | 2.067812 | 0.981570 | 0.317759 | 0.885154 |
| 18 | 1.007385 | 1.699621 | 1.133951 | 2.047354 | 0.576223 | 0.837475 | 1.982129 | 1.243612 | 0.337080 | 0.854811 |
| 19 | 1.115520 | 1.667761 | 0.884498 | 2.162010 | 0.664477 | 1.182708 | 1.967366 | 1.221843 | 0.400818 | 0.921774 |
Number of students travelling more than 3 miles per run
| Blatchington Mill School | Brighton Aldridge Community Academy | Cardinal Newman Catholic School | Dorothy Stringer School | Hove Park School and Sixth Form Centre | King's School | Longhill High School | Patcham High School | Portslade Aldridge Community Academy | Varndean School | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 16 | 0 | 0 | 74 | 0 | 0 | 82 | 0 | 0 | 0 |
| 1 | 9 | 0 | 16 | 59 | 0 | 0 | 88 | 0 | 0 | 0 |
| 2 | 23 | 0 | 0 | 56 | 0 | 0 | 74 | 0 | 0 | 0 |
| 3 | 15 | 0 | 0 | 57 | 0 | 0 | 101 | 0 | 0 | 0 |
| 4 | 15 | 0 | 0 | 83 | 0 | 0 | 64 | 0 | 0 | 0 |
| 5 | 37 | 0 | 16 | 42 | 0 | 0 | 79 | 0 | 0 | 0 |
| 6 | 50 | 0 | 0 | 87 | 0 | 0 | 114 | 0 | 0 | 0 |
| 7 | 0 | 0 | 0 | 43 | 0 | 0 | 68 | 0 | 0 | 0 |
| 8 | 46 | 0 | 0 | 105 | 0 | 0 | 84 | 0 | 0 | 0 |
| 9 | 18 | 0 | 0 | 57 | 0 | 0 | 87 | 0 | 0 | 0 |
| 10 | 7 | 0 | 0 | 92 | 0 | 0 | 38 | 0 | 0 | 0 |
| 11 | 0 | 0 | 0 | 102 | 0 | 0 | 130 | 0 | 0 | 0 |
| 12 | 15 | 0 | 14 | 44 | 0 | 0 | 90 | 0 | 0 | 0 |
| 13 | 9 | 0 | 0 | 58 | 0 | 0 | 115 | 0 | 0 | 0 |
| 14 | 11 | 0 | 18 | 73 | 0 | 0 | 85 | 0 | 0 | 0 |
| 15 | 0 | 0 | 16 | 63 | 0 | 0 | 104 | 0 | 0 | 0 |
| 16 | 51 | 0 | 16 | 60 | 0 | 0 | 141 | 0 | 0 | 0 |
| 17 | 20 | 0 | 12 | 61 | 0 | 0 | 93 | 0 | 0 | 0 |
| 18 | 0 | 0 | 17 | 60 | 0 | 0 | 80 | 0 | 0 | 0 |
| 19 | 15 | 0 | 0 | 85 | 0 | 0 | 80 | 0 | 0 | 0 |
## plot target PAN numbers
## create ordered lists
distances, distances_outside_catchment, students_3_miles = list(), list(), list()
x_axis_list = list()
for school_str in dist_df.columns:
## find all LSOAs outside the catchment
LSOAs_outside_catchment = students[(students["school"] == school_str) & (students["catchment_ID"] != students["catchment_ID_school"])]
if target_PAN[school_str] == 0: distances.append(0)
else: distances.append(statistics.mean(dist_df[school_str]))
if len(LSOAs_outside_catchment.index) > 0:
distances_outside_catchment.append(statistics.mean(dist_external_df[school_str]))
else:
distances_outside_catchment.append(0)
students_3_miles.append(statistics.mean(students_3_miles_df[school_str]))
x_axis_list.append(school_str)
## Generate the graphs
x_axis = np.arange(len(x_axis_list))
width = 0.25
fig = plt.figure(figsize=(14,7))
ax1 = fig.add_subplot(121)
ax2 = fig.add_subplot(122)
ax1.set_title("Graph 3.1: Number of students travelling more than 3 miles")
ax1.bar(x_axis, students_3_miles, width, color="red")
ax1.set_xticks(x_axis, x_axis_list, rotation=90)
ax1.set_ylabel("Number of students")
ax2.set_title("Graph 3.2: Mean distances travelled")
ax2.bar(x_axis - width / 2 , distances, width, color="steelblue", label="All students")
ax2.bar(x_axis + width / 2 , distances_outside_catchment, width, color="red", label="Students outside catchment")
ax2.set_xticks(x_axis, x_axis_list, rotation=90)
ax2.set_ylabel("Distance (miles)")
ax2.legend()
plt.show()
plt.close()